﻿using NLog;
using NLog.Config;
using NLog.Targets;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using TiaoTiaoCode.NLogger.Dtos;
using TiaoTiaoCode.NLogger.Enums;

namespace TiaoTiaoCode.NLogger
{
    /// <summary>
    /// NLog帮助类
    /// 
    /// Step:
    ///     1、创建Target：InfoTarget ErrorTarget DatabaseTarget
    ///     2、创建配置：GetConfig
    ///     3、初始化：InitLogger
    /// 或
    ///     直接使用 FastNoDatabaseInit
    /// </summary>
    public class TiaoTiaoNLogger
    {
        public const string DefaultLoggerName = "TiaoTiaoLogger";

        #region 字典对象

        /// <summary>
        /// Logger字典对象
        /// </summary>
        private static readonly ConcurrentDictionary<string, Logger> _dicLogger = new();

        /// <summary>
        /// 与 Target 对应的Rule
        /// </summary>
        private static readonly ConcurrentDictionary<string, List<LoggingRule>> _dicRule = new();

        /// <summary>
        /// 与 LoggerName对应的配置
        /// </summary>
        private static readonly ConcurrentDictionary<string, LoggingConfiguration> _dicConfig = new();
        #endregion

        #region

        /// <summary>
        /// Log 对象
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        static Logger Logger(string key)
        {
            Logger log;
            if (_dicLogger.ContainsKey(key))
                _dicLogger.TryGetValue(key, out log);
            else
            {
                log = LogManager.GetLogger(key);
                _dicLogger.TryAdd(key, log);
            }

            LoggingConfiguration cfg;
            if (_dicConfig.ContainsKey(key))
                _dicConfig.TryGetValue(key, out cfg);
            else
                throw new Exception("无Logger配置");

            log.Factory.Configuration = cfg;

            return log;
        }

        /// <summary>
        /// 更新Log对象
        /// </summary>
        /// <param name="key"></param>
        /// <param name="log"></param>
        static void Logger(string key, Logger log)
        {
            _dicLogger.AddOrUpdate(key, log, (k, v) =>
            {
                if (k == key) return log;
                return v;
            });
        }

        #endregion

        #region NLog内部常量

        const string DatabaseTargetName = "Database";

        #endregion

        #region NLogger配置项

        /// <summary>
        /// 添加文件型Target
        /// </summary>
        /// <param name="loggerName">日志对象名</param>
        /// <param name="targetName"></param>
        /// <param name="logLevel">日志级别</param>
        /// <param name="logFileSplitName">log文件的后缀设置名 如：_opc _debug _info</param>
        /// <param name="logPath"></param>
        /// <param name="layout"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        public static void AddFileTarget(string loggerName, string targetName, LogLevel logLevel,
            string? logFileSplitName = null, string? logPath = null, string? layout = null,
            int autoClearDayLen = 15, int aboveSize = 10485760)
        {
            if (string.IsNullOrEmpty(loggerName))
                throw new Exception("Logger名为空");

            var logFileName = string.IsNullOrWhiteSpace(logFileSplitName)
                            ? $"${{date:format=yyyyMMdd}}.log"
                            : $"${{date:format=yyyyMMdd}}_{logFileSplitName}.log";

            var target = new FileTarget(targetName)
            {
                FileName = string.IsNullOrWhiteSpace(logPath)
                              ? $"Logs/{logFileName}"
                              : $"{logPath}/{logFileName}",
                Layout = string.IsNullOrWhiteSpace(layout)
                            ? "【${longdate}】  【${level}】${newline}\t${message}${newline}"
                            : layout,
                MaxArchiveDays = autoClearDayLen,
                ArchiveAboveSize = aboveSize,
                ArchiveNumbering = ArchiveNumberingMode.Sequence
            };

            var rule = new LoggingRule("*", logLevel, logLevel, target)
            {
                Final = true
            };

            if (_dicRule.ContainsKey(loggerName))
            {
                _dicRule.TryGetValue(loggerName, out var lst);
                lst.Add(rule);
            }
            else
            {
                var lst = new List<LoggingRule> { rule };
                _dicRule.TryAdd(loggerName, lst);
            }
        }

        /// <summary>
        /// 配置写入数据库日志
        /// </summary>
        /// <param name="dbType"></param>
        /// <param name="connectString"></param>
        /// <param name="commandText"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public static void AddDatabaseTarget(string loggerName, NLogDbType dbType, string connectString, string commandText, Type type)
        {
            var target = new DatabaseTarget(DatabaseTargetName)
            {
                DBProvider = dbType switch
                {
                    NLogDbType.SqlServer => "Microsoft.Data.SqlClient.SqlConnection, Microsoft.Data.SqlClient",
                    NLogDbType.MySql => "MySql.Data.MySqlClient.MySqlConnection, MySql.Data",
                    NLogDbType.Sqlite => "Microsoft.Data.Sqlite.SQLiteConnection, Microsoft.Data.Sqlite",
                    _ => "MySql.Data.MySqlClient.MySqlConnection, MySql.Data"
                },
                ConnectionString = connectString,
                CommandText = commandText,
                CommandType = System.Data.CommandType.Text
            };

            var properties = type.GetProperties();

            foreach (var p in properties)
            {
                target.Parameters.Add(new DatabaseParameterInfo
                {
                    Name = $"@{p.Name}",
                    Layout = $"${{event-properties:{p.Name}}}"
                });
            }

            if (_dicRule.ContainsKey(loggerName))
            {
                _dicRule.TryGetValue(loggerName, out var lst);
                lst.Add(new LoggingRule("*", LogLevel.Trace, target) { Final = true });
            }
            else
            {
                var lst = new List<LoggingRule> { new LoggingRule("*", LogLevel.Trace, target) { Final = true } };
                _dicRule.TryAdd(loggerName, lst);
            }
        }

        #endregion

        #region 初始化

        /// <summary>
        /// 快速设置无数据库的Logger
        /// </summary>
        public static void FastNoDatabaseInit()
        {
            AddFileTarget(DefaultLoggerName, "info", LogLevel.Info, "info");
            AddFileTarget(DefaultLoggerName, "info", LogLevel.Error, "error");
            AddFileTarget(DefaultLoggerName, "debug", LogLevel.Info, "debug");

            if (!_dicRule.TryGetValue(DefaultLoggerName, out var lst))
                throw new Exception("获取日志配置失败");

            var cfg = new LoggingConfiguration();

            lst.ForEach(t => cfg.AddRule(t));

            if (_dicConfig.ContainsKey(DefaultLoggerName))
            {
                _dicConfig.TryGetValue(DefaultLoggerName, out var tmp);
                tmp = cfg;
            }
            else
            {
                _dicConfig.TryAdd(DefaultLoggerName, cfg);
            }

            var logger = Logger(DefaultLoggerName);
            Logger(DefaultLoggerName, logger);
        }

        /// <summary>
        /// 设置Logger
        /// </summary>
        /// <param name="loggerName"></param>
        /// <exception cref="Exception"></exception>
        public static void SetLogger(string loggerName)
        {
            if (!_dicRule.TryGetValue(loggerName, out var lst))
                throw new Exception("获取日志配置失败");

            var cfg = new LoggingConfiguration();

            lst.ForEach(t =>
            {
                foreach (var x in t.Targets)
                {
                    cfg.AddTarget(x);
                }
                cfg.AddRule(t);
            });

            if (_dicConfig.ContainsKey(loggerName))
            {
                _dicConfig.TryGetValue(loggerName, out var tmp);
                tmp = cfg;
            }
            else
            {
                _dicConfig.TryAdd(loggerName, cfg);
            }

            var logger = Logger(loggerName);
            Logger(loggerName, logger);
        }

        #endregion

        #region 日志方法

        /// <summary>
        /// 普通消息日志
        /// </summary>
        /// <param name="msg"></param>
        public static void LogInfo(string msg, string loggerName = DefaultLoggerName)
        {
            var logger = Logger(loggerName);
            logger.Info(msg);
        }

        /// <summary>
        /// Debug日志
        /// </summary>
        /// <param name="msg"></param>
        public static void LogDebug(string msg, string loggerName = DefaultLoggerName)
        {
            var logger = Logger(loggerName);
            logger.Debug(msg);
        }

        /// <summary>
        /// 异常日志
        /// </summary>
        /// <param name="msg"></param>
        /// <param name="ex"></param>
        public static void LogError(string msg, Exception? ex = null, string loggerName = DefaultLoggerName)
        {
            var logger = Logger(loggerName);
            logger.Error(msg, ex);
        }

        /// <summary>
        /// 数据库日志
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="dbLogItem"></param>
        public static void LogDb<T>(T dbLogItem, string loggerName = DefaultLoggerName) where T : LogItem
        {
            var logger = Logger(loggerName);

            if (logger.Factory.Configuration.FindTargetByName(DatabaseTargetName) is not DatabaseTarget dbTarget) return;

            var parameters = dbTarget.Parameters;

            var type = typeof(T);
            var properties = type.GetProperties().ToList();

            var li = new LogEventInfo(LogLevel.Trace, "", "");

            foreach (var p in parameters)
            {
                var prop = properties.FirstOrDefault(t => t.Name.ToUpper() == p.Name.ToUpper().TrimStart('@'));
                if (prop == null)
                    li.Properties[p.Name.TrimStart('@')] = null;
                else
                {
                    if (prop.PropertyType.Name.ToUpper().Contains("DATETIME"))
                        li.Properties[p.Name.TrimStart('@')] = DateTime.Parse(prop.GetValue(dbLogItem).ToString()).ToString("yyyy-MM-dd HH:mm:ss");
                    else
                        li.Properties[p.Name.TrimStart('@')] = prop.GetValue(dbLogItem);
                }
            }

            logger.Trace(li);
        }

        #endregion
    }
}
